#light
namespace FSharp.WebTools.ClientControls

open System
open System.IO
open System.Text
open System.Web
open System.Web.UI

open Microsoft.FSharp.Quotations.Typed // "lift"

open FSharp.WebTools
open FSharp.WebTools.Core
open FSharp.WebTools.ClientControls
open FSharp.WebTools.ClientLibrary

type CalendarInfo = { FirstDayOfWeek:int }
type MonthInfo = { FirstMonthDay:int; TotalDays:int; Today:int option; Name:string; }

type HighlightDatesEventArgs(m,y) = 
  inherit EventArgs()
  let dates = new ResizeArray<int>();
  member x.Month = m
  member x.Year = y
  member x.Add(i) = dates.Add(i)
  member x.SelectedDays = dates
  
[<MixedSide>]
type Calendar = 
  inherit ClientControl as base
  interface INamingContainer
  
  val mutable lnkNext  : LinkButton  
  val mutable lnkPrev  : LinkButton 
  val mutable lblMonth : Label
  
  val raiseHighlightDates : Calendar * HighlightDatesEventArgs -> unit
  val highlightDates : IHandlerEvent<HighlightDatesEventArgs>
   
  new() = 
    let dt = DateTime.Now;
    let r, e = IEvent.create_HandlerEvent<_, _>()
    let fnull() = (Array.zero_create 1).[0]
    { clientSelected = (Array.zero_create 1).[0]; monthInfo=fnull();
      date = dt; calendarInfo=fnull(); month=1; year=2000;
      lnkNext=fnull(); lnkPrev=fnull(); lblMonth=fnull();
      raiseHighlightDates = r; highlightDates = e; 
      highlightedDays = fnull(); renderAsLink = false; }

  // ----------------------------------------------------------------------------------------------
  // Duplex: fields to store current state

  // render anchors as hyperlinks?
  [<DuplexField>]
  val mutable renderAsLink : bool

  // highlighted date
  [<DuplexField>]
  val mutable date : DateTime
  
  // currently displayed month & year
  [<DuplexField>]
  val mutable month : int
  [<DuplexField>]
  val mutable year : int  
  
  // info about calendar & about the current month
  [<DuplexField>]
  val mutable calendarInfo : CalendarInfo
  [<DuplexField>]
  val mutable monthInfo : MonthInfo

  [<DuplexField>]
  val mutable highlightedDays : ResizeArray<int>

  // ----------------------------------------------------------------------------------------------
  // Properties:
  
  [<ReflectedDefinition>] 
  member this.RenderAsLink 
    with get() = duplex { return this.renderAsLink }
    and  set(v) = duplex { do! <@! (_:Calendar).renderAsLink <- _ !@> (lift this) (lift v) }
  
  // ----------------------------------------------------------------------------------------------
  // Server side: information update
  
  member this.HighlightDates = 
    server 
      { return this.highlightDates }
  
  member this.UpdateHighlightedDates() =
    server
      { let e = new HighlightDatesEventArgs(this.month, this.year)
        do  this.raiseHighlightDates(this, e)
        do! <@! (_ : Calendar).highlightedDays <- (_) !@> (lift this) (lift e.SelectedDays) }
        
  member this.UpdateCalendarInfo() =
    server
      { let cult = System.Globalization.CultureInfo.CurrentCulture;
        let wtn = fun (w:DayOfWeek) -> ((unbox (box w)):int)
        let ci = { FirstDayOfWeek=wtn (cult.DateTimeFormat.FirstDayOfWeek); }
        do! <@! (_ : Calendar).calendarInfo <- (_) !@> (lift this) (lift ci) }

  member this.UpdateMonthInfo() =
    server
      { let wtn = fun (w:DayOfWeek) -> ((unbox (box w)):int)
        let cal = new System.Globalization.GregorianCalendar();
        let totDays = cal.GetDaysInMonth(this.year, this.month)
        let today = if (cal.GetMonth(this.date) = this.month) then Some(cal.GetDayOfMonth(this.date)) else None        
        let mi= { FirstMonthDay = wtn (cal.GetDayOfWeek(new System.DateTime(this.year, this.month, 1)));
             Today = today; TotalDays = totDays; Name = (sprintf "%d/%d" this.month this.year) }
        do! <@! (_ : Calendar).monthInfo <- (_) !@> (lift this) (lift mi) }
  
  // ----------------------------------------------------------------------------------------------
  // Server side: initialization
               
  override this.OnInit (e:System.EventArgs) =
    this.lnkNext <- new LinkButton()
    this.lnkNext.Text <- "&gt;"
    this.lnkNext.ID <- "lnkNext" 
    this.Controls.Add(this.lnkNext)

    this.lnkPrev <- new LinkButton()
    this.lnkPrev.Text <- "&lt;"
    this.lnkPrev.ID <- "lnkPrev" 
    this.Controls.Add(this.lnkPrev)
    
    this.lblMonth <- new Label()
    this.lblMonth.ID <- "lblMonth" 
    this.Controls.Add(this.lblMonth)

    execute (this.UpdateCalendarInfo())
    execute (this.UpdateMonthInfo())
    execute (this.UpdateHighlightedDates())
    
    base.OnInit(e);

  // ----------------------------------------------------------------------------------------------
  // Client side: initialization & exposed client-side & mixed properties

  [<ReflectedDefinition>]
  member this.Client_Load ((sender:Object),(e:ClientLibrary.EventArgs)) =
    client 
      { do  this.lnkNext.ClientClick.AddClient(fun (s, e) -> this.MoveMonth(1, s, e))
        do  this.lnkPrev.ClientClick.AddClient(fun (s, e) -> this.MoveMonth(-1, s, e))
        do! this.AttachHandlers(); }

  [<WebBehavior(WebBehavior.ClientConstructor); ReflectedDefinition>]
  member this.ClientConstructor(el:#IElement) = // trick .. 'el' is needed by translator
    client
      { do this.clientSelected <- new ClientEvent(); }

  [<WebBehavior(WebBehavior.ClientSide)>]
  val mutable clientSelected : ClientEvent;
  
  [<ReflectedDefinition>]
  member this.ClientDateSelected = 
    client { return this.clientSelected }

  [<ReflectedDefinition>]  
  member this.RefreshView () =
    makeDuplex
      (server 
        { do! this.UpdateMonthInfo();
          do! this.UpdateHighlightedDates(); })
      (client 
        { do! asyncExecute
               (client_async
                 { do! serverExecute (this.RefreshView());
                   do! this.ClientRender(); }) })
      
  [<ReflectedDefinition>]  
  member this.SelectedDate 
    with get() = duplex { return this.date }
    and  set(d) = 
      duplex 
        { do! <@! (_:Calendar).date <- _ !@> (lift this) (lift d) 
          do! <@! (_:Calendar).month <- _ !@> (lift this) (lift d.Month) 
          do! <@! (_:Calendar).year <- _ !@> (lift this) (lift d.Year) 
          do! this.RefreshView(); }

  // ----------------------------------------------------------------------------------------------
  // Client side: Date selection, Month movement

  [<ReflectedDefinition>]
  member this.GetDayElement(i) =
    client
      { return match this.GetChild("day"+Convert.ToString(i)) with
               | Some el -> el
               | _ -> failwith "Element not found!" }
    
  [<ReflectedDefinition>]
  member this.AttachHandlers () =
    client 
      { do! client_for (1, this.monthInfo.TotalDays) (fun i -> 
              client 
                { let! el = this.GetDayElement(i);
                  do   el.ClientClick.AddClient(fun (s,e) -> 
                        this.DayClicked(0+i, e)) }) }
            
  [<ReflectedDefinition>]            
  member this.DayClicked(i:int, e:CancelEventArgs) =
    client
      { let dt = this.date
        let y = dt.Year
        let d = dt.Day
        let m = dt.Month
        let! oel = this.GetDayElement(d)
        let! nel = this.GetDayElement(i)
        do e.Canceled <- true;
           if (m = this.month && y = this.year) then oel.RemoveCssClass("sel")
           nel.AddCssClass("sel")
           this.date <- new DateTime(this.year, this.month, i, 0, 0, 0, 0);
           this.clientSelected.Invoke(this, new EventArgs()); }

  [<ReflectedDefinition>]
  member this.MoveMonth (direction, sender, e:ClientLibrary.CancelEventArgs) =
    client 
      { do e.Canceled <- true;
        let m = this.month + direction;
        let m, y =
            match m with
            | 0   ->  12, this.year - 1 
            | 13  ->  1, this.year + 1
            | _ -> m, this.year
        do this.month <- m
        do this.year <- y
        do! this.RefreshView(); }
           
  // ----------------------------------------------------------------------------------------------
  // Mixed: Rendering
      
  [<ReflectedDefinition>] 
  member this.RenderMonth(wr:HtmlTextWriter, clid) = 
    duplex 
      { do
          let renderSpacer wd = 
            wr.Write(sprintf "<div class=\"spc swd%d\">&#160;</div>" wd);
          let renderDay (wd,n,hl) = 
            let hlcls = if (hl) then " hl" else ""
            if (this.renderAsLink) then
              wr.Write(sprintf "<div id=\"%s_day%d\" class=\"wd%d%s\"><a href=\"?y=%d&amp;m=%d&amp;d=%d\">%d</a></div>" 
                clid n wd hlcls this.year this.month n n);
            else
              wr.Write(sprintf "<div id=\"%s_day%d\" class=\"wd%d%s\"><a href=\"#\">%d</a></div>" 
                clid n wd hlcls n);
          let renderSelDay (wd,n,hl) = 
            let hlcls = if (hl) then " sel_hl" else ""
            if (this.renderAsLink) then
              wr.Write(sprintf "<div id=\"%s_day%d\" class=\"wd%d sel%s\"><a href=\"?y=%d&amp;m=%d&amp;d=%d\">%d</a></div>" 
                clid n wd hlcls this.year this.month n n);
            else
              wr.Write(sprintf "<div id=\"%s_day%d\" class=\"wd%d sel%s\"><a href=\"#\">%d</a></div>" 
                clid n wd hlcls n);
          let renderWeekStart () = 
            wr.Write("<div class=\"week\">");
          let renderWeekEnd () = 
            wr.Write("</div>");

          let fixwd wd = if wd = 0 then 7 else wd
          let weekStart = this.calendarInfo.FirstDayOfWeek%7
          let firstDay = this.monthInfo.FirstMonthDay
          let today = match this.monthInfo.Today with Some n -> n | _ -> -1; 

          renderWeekStart()
          for i = 1 to (((firstDay-weekStart)+7)%7) do 
            renderSpacer (fixwd ((weekStart+i-1)%7))
            
          for i = 1 to this.monthInfo.TotalDays do 
            let wd = fixwd ((firstDay+i-1)%7)
            let hl = this.highlightedDays.Contains(i)
            if (i = today) then renderSelDay(wd,i,hl) else renderDay(wd,i,hl)
            if ((firstDay+i)%7 = weekStart) then 
              renderWeekEnd()
              if (i <> this.monthInfo.TotalDays) then renderWeekStart()
              
          let pad = (7-(firstDay + this.monthInfo.TotalDays - weekStart)%7)%7
          for i = 1 to pad do 
            renderSpacer (fixwd ((this.monthInfo.FirstMonthDay + this.monthInfo.TotalDays + i - 1)%7))
            
          if (pad >= 1) then renderWeekEnd() }

  [<ReflectedDefinition>]
  member this.ClientRender() =
    client
      { let sb = new StringBuilder();
        do!  this.lblMonth.set_ClientText(this.monthInfo.Name);
        do! this.RenderMonth(new HtmlTextWriter(new StringWriter(sb)), (this :> IElement).ClientID);
        do  match this.GetChild("content") with
            | Some el -> el.InnerHtml <- sb.ToString();
            | _ -> failwith "Element not found!" 
        do! this.AttachHandlers(); }
        
  member this.ServerRender(wr:HtmlTextWriter) =
    server
      { do!  this.lblMonth.set_ClientText(this.monthInfo.Name);
        do   wr.Write("<div class=\"{1}\" id=\"{0}\">", this.ClientID, this.CssClass);
             wr.Write("<div class=\"nav nav_prev\">");
             this.lnkPrev.RenderControl(wr);
             wr.Write("</div><div class=\"nav nav_next\">");
             this.lnkNext.RenderControl(wr);
             wr.Write("</div><div class=\"header_month\">", this.ClientID);
             this.lblMonth.RenderControl(wr);
             wr.Write("</div><div id=\"{0}_content\">", this.ClientID);
        do!  this.RenderMonth(wr, this.ClientID);
        do   wr.Write("</div></div>"); }

  override this.Render(wr) =
    execute (this.ServerRender(wr));